﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.ObjectModel;


namespace Xceed.Silverlight.Data
{
  internal static class ComplexStringHelper
  {
    internal static PropertyInfo GetPropertyInfo( Type currentType, string data )
    {
      PropertyInfo propertyInfo = null;
      string[] stringArray = data.Split( m_separators );

      foreach( string currentString in stringArray )
      {
        if( currentString.Contains( "]" ) )
        {
          Type indexerParameterType = null;
          PropertyInfo indexer = ComplexStringHelper.GetIndexer( currentType, out indexerParameterType );
          if( indexer == null )
          {
            return null;
          }
          currentType = indexer.PropertyType;
        }
        else
        {
          propertyInfo = currentType.GetProperty( currentString );
          if( propertyInfo == null )
          {
            return null;
          }
          currentType = propertyInfo.PropertyType;
        }
      }

      return propertyInfo;
    }

    internal static Type GetExpressionTypeFromString( string stringToEvaluate, Type dataType )
    {
      bool isNullableExpression;
      Expression dataExpression = Expression.Parameter( typeof( object ), null );
      dataExpression = ComplexStringHelper.GetExpressionFromString( stringToEvaluate, dataType, dataExpression, false, out isNullableExpression );

      if( ( dataExpression == null ) || ( dataExpression.Type == null ) )
        return null;

      if( isNullableExpression && !dataExpression.Type.IsNullableType() )
        return typeof( Nullable<> ).MakeGenericType( dataExpression.Type );

      return dataExpression.Type;
    }

    internal static Expression GetExpressionFromString( string stringToEvaluate, Type dataType, Expression intialExpression, bool isCheckingNull, out bool isNullableExpression )
    {
      isNullableExpression = false;

      Expression dataExpression = intialExpression;
      if( !dataType.IsAssignableFrom( intialExpression.Type ) )
      {
        dataExpression = Expression.Convert( intialExpression, dataType );
      }

      if( stringToEvaluate == "." )
        return dataExpression;

      string[] stringArray = stringToEvaluate.Split( m_separators );

      if( isCheckingNull )
      {
        dataExpression = ComplexStringHelper.GetExpressionWithNullCheck( stringArray, dataType, dataExpression );
      }
      else
      {
        dataExpression = ComplexStringHelper.GetPropertyField( dataExpression, stringArray, stringArray.Length - 1, out isNullableExpression );       
      }

      return dataExpression;
    }

    private static Expression GetExpressionWithNullCheck( string[] stringArray, Type dataType, Expression dataExpression )
    {
      Expression nullExpr = Expression.Convert( Expression.Constant( null ), typeof( object ) );

      if( stringArray.Length == 0 )
        return nullExpr;

      BlockExpression[] currentExpressionCheckNull = new BlockExpression[ stringArray.Length ];
      LabelTarget endLabel = Expression.Label( typeof( object ), "endLabel" );

      ParameterExpression param = Expression.Variable( typeof( object ), "sampleVar" );
      Expression currentExpr;
      Expression currentExprObject;
      bool isNullableExpression;
      BlockExpression tryBlock = Expression.Block( currentExpr = ComplexStringHelper.GetPropertyField( dataExpression, stringArray, stringArray.Length - 1, out isNullableExpression ),
                                                   currentExprObject = Expression.Convert( currentExpr, typeof( object ) ),
                                                   Expression.Assign( param, currentExprObject ) );
      CatchBlock catchBlock = Expression.Catch( typeof( Exception ), Expression.Assign( param, nullExpr ) );
      TryExpression tryExpr = Expression.TryCatch( tryBlock, catchBlock );

      ConditionalExpression condExpr = Expression.IfThenElse( Expression.ReferenceEqual( param, nullExpr ),
                                                                                         Expression.Return( endLabel, nullExpr ),
                                                                                         Expression.Return( endLabel, param ) );

      currentExpressionCheckNull[ currentExpressionCheckNull.Length - 1 ] = Expression.Block( new ParameterExpression[] { param }, 
                                                                                              tryExpr, 
                                                                                              condExpr );

      // Starting from last second string part, build the final block conditional expression
      // returning null when finding a null string property or an exception in GetPropertyField. 
      // For example, if looking for o.Name1.Name2.Name3, the final expression should look like 
      // (with the try-catch around each properties):
      // if(o.Name1 == null)
      //    return null;
      // else
      // {
      //   if(o.Name1.Name2 == null)
      //    return null;
      //   else
      //   {
      //      if(o.Name1.Name2.Name3 == null)
      //         return null;
      //      else
      //         return o.Name1.Name2.Name3;
      //   }
      // }
      // We need to start from the second last string part, because the Else of the Expression.Condition
      // will be constructed with the following string part.
      for( int i = stringArray.Length - 2; i >= 0; --i )
      {
        tryBlock = Expression.Block( currentExpr = ComplexStringHelper.GetPropertyField( dataExpression, stringArray, i, out isNullableExpression ),
                                     currentExprObject = Expression.Convert( currentExpr, typeof( object ) ),
                                     Expression.Assign( param, currentExprObject ) );
        catchBlock = Expression.Catch( typeof( Exception ), Expression.Assign( param, nullExpr ) );
        tryExpr = Expression.TryCatch( tryBlock, catchBlock );

        condExpr = Expression.IfThenElse( Expression.ReferenceEqual( param, nullExpr ),
                                                                     Expression.Return( endLabel, nullExpr ),
                                                                     currentExpressionCheckNull[ i + 1 ] );

        currentExpressionCheckNull[ i ] = Expression.Block( new ParameterExpression[] { param },
                                                            tryExpr,
                                                            condExpr );
      }

      BlockExpression block = Expression.Block( currentExpressionCheckNull[ 0 ],
                                                Expression.Label( endLabel, nullExpr ) ); 

      return block;
    }

    private static Expression GetPropertyField( Expression dataExpression, string[] stringArray, int index, out bool isNullableExpression )
    {
      isNullableExpression = false;
      int i = 0;

      foreach( string stringPart in stringArray )
      {
        if( stringPart.IndexOf( ']' ) != -1 )
        {
           int endIndex = stringPart.IndexOf( ']' );

          //TODO : Manage if there are multiple brackets for this indexer (e.g. "[a][b]"). 

          string indexString = stringPart.Substring( 0, endIndex );
          if( indexString.IndexOf( ',' ) != -1 )
          {
            // TODO : Manage if there are multiple parameters specified for this indexer (e.g. "[a,b]").
            return null;
          }

          Type indexerParameterType = null;
          PropertyInfo indexer = ComplexStringHelper.GetIndexer( dataExpression.Type, out indexerParameterType );
          if( indexer == null )
          {
            throw new InvalidOperationException( "Unable to retrieve the indexer." );
          }

          object id = Convert.ChangeType( indexString, indexerParameterType, null );

          dataExpression = Expression.Property( dataExpression, indexer, Expression.Constant( id ));

          isNullableExpression = true;
        }
        else
        {
          try
          {
            // associated with the stringPart; it will become the new dataExpression.
            dataExpression = Expression.PropertyOrField( dataExpression, stringPart );
          }
          catch( Exception )
          {
            return null;
          }

          if( dataExpression.Type.IsNullableType() )
            isNullableExpression = true;
        }

        if( i++ == index )
        {
          return dataExpression;
        }
      }

      throw new InvalidOperationException( "Unable to retrieve the selected property or field." );
    }

    private static PropertyInfo GetIndexer( Type type, out Type indexerParameterType )
    {
      indexerParameterType = null;
      ObservableCollection<PropertyInfo> indexerPropertyCollection = new ObservableCollection<PropertyInfo>();

      PropertyInfo[] properties = type.GetProperties();
      if( ( properties != null ) && ( properties.Length > 0 ) )
      {
        foreach( PropertyInfo property in properties )
        {
          if( property.GetIndexParameters().Length > 0 )
          {
            indexerPropertyCollection.Add( property );
          }
        }

        if( indexerPropertyCollection.Count > 0 )
        {
          //TODO : need to chose an indexer property if more than one
          int currentIndexerProperty = 0;
          //TODO : need to chose an indexer parameter if more than one
          int currentIndexerParameter = 0;

          ParameterInfo[] indexerParameterInfo = indexerPropertyCollection[ currentIndexerProperty ].GetIndexParameters();
          indexerParameterType = indexerParameterInfo[ currentIndexerParameter ].ParameterType;
          return indexerPropertyCollection[ currentIndexerProperty ];
        }
      }

      return null;
    }

    private static char[] m_separators = new char[] { '.', '[' };
  }
}
